package com.atguigu.gmall.index.service;

import com.alibaba.fastjson.JSON;
import com.atguigu.gmall.common.bean.ResponseVo;
import com.atguigu.gmall.index.annotation.GmallCache;
import com.atguigu.gmall.index.feign.GmallPmsClient;
import com.atguigu.gmall.index.utils.DistributedLock;
import com.atguigu.gmall.pms.entity.CategoryEntity;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @Author 刘嘉乐
 * @Date 2022/1/9 19:58
 * @Version 1.0
 */
@Service
public class IndexService {
    @Autowired
    private GmallPmsClient gmallPmsClient;

    @Autowired
    private StringRedisTemplate redisTemplate;
    private static final String KEY_PREFIX = "index:cates:";
    private static final String LOCK_PREFIX = "index:cates:lock:";

    public List<CategoryEntity> queryLvl1Categories() {
        ResponseVo<List<CategoryEntity>> listResponseVo = this.gmallPmsClient.queryCategoriesByPid(0l);
        return listResponseVo.getData();
    }
    @Autowired
    private RedissonClient redissonClient;

    /*
     使用自定义注解的方式解决缓存击穿和雪崩  redisson + aop + 自定义注解
    */
    @GmallCache(prefix = KEY_PREFIX, timeout = 129600, random = 14400, lock = LOCK_PREFIX)
    public List<CategoryEntity> queryLvl23CatesByPid(Long pid) {
        ResponseVo<List<CategoryEntity>> listResponseVo = this.gmallPmsClient.queryCategoriesWithSubByPid(pid);
        System.out.println("3目标方法执行。。。。。");
        return listResponseVo.getData();
    }


/*
 这种是使用分布式锁解决缓存击穿 和雪崩     代码比较多    用的redisson框架：基于redis
*/
    public List<CategoryEntity> queryLvl23CatesByPid2(Long pid) {
        // 1.先查询缓存，如果缓存命中则直接返回
        String json = this.redisTemplate.opsForValue().get(KEY_PREFIX + pid);
        if (StringUtils.isNotBlank(json)) {
            return JSON.parseArray(json, CategoryEntity.class);
        }

        // 为了防止缓存击穿，添加分布式锁
        RLock fairLock = this.redissonClient.getFairLock(LOCK_PREFIX + pid);
        fairLock.lock();

        try {
            // 在获取锁的过程中，可能有其他请求已经把数据放入缓存，需要再次确认缓存有没有
            String json2 = this.redisTemplate.opsForValue().get(KEY_PREFIX + pid);
            if (StringUtils.isNotBlank(json2)) {
                return JSON.parseArray(json2, CategoryEntity.class);
            }

        // 2.调用远程接口，获取数据，放入缓存
        ResponseVo<List<CategoryEntity>> listResponseVo = this.gmallPmsClient.queryCategoriesWithSubByPid(pid);
        List<CategoryEntity> categoryEntities = listResponseVo.getData();
        // 放入缓存，为了防止缓存穿透，数据即使为null也缓存
        if (!CollectionUtils.isEmpty(categoryEntities)) {
            // 为了防止缓存雪崩，给缓存时间添加随机值
            this.redisTemplate.opsForValue().set(KEY_PREFIX + pid, JSON.toJSONString(categoryEntities), 90 + new Random().nextInt(10), TimeUnit.DAYS);
        } else {
            this.redisTemplate.opsForValue().set(KEY_PREFIX + pid, JSON.toJSONString(categoryEntities), 5, TimeUnit.MINUTES);
        }
        return categoryEntities;
        } finally {
            fairLock.unlock();
        }
    }




   /*
     单节点启动不会发送超卖
        测试超卖问题
       设置多个实例测试
       用ab进行测试
   */

   //  用了redis实现分布式锁
    public  void testLock1() {
         //  2 往redis设置唯一标识
        String uuid = UUID.randomUUID().toString();
        //加锁         1  并设置了拿锁后过期时间  可以防止一台获得锁的机器宕机后导致死锁问题
        Boolean look = this.redisTemplate.opsForValue().setIfAbsent("look", uuid,3, TimeUnit.SECONDS);
                                   // redis里的 setnx 实现独占排他
        if (!look){
            try {
                Thread.sleep(50);
                this.testLock1();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            // 获取到锁之后，执行业务操作
            String num = this.redisTemplate.opsForValue().get("num");
            if (StringUtils.isBlank(num)) {
                this.redisTemplate.opsForValue().set("num", "1");
                return;
            }
            int i = Integer.parseInt(num);
            this.redisTemplate.opsForValue().set("num", String.valueOf(++i));
            //释放锁              2通过uuid设置锁判断是否是自己的锁
            if (StringUtils.equals(uuid,this.redisTemplate.opsForValue().get("look")))
            this.redisTemplate.delete("look");

            // 1 加了获取锁过期时间  防止死锁
            //  2  设置了 uuid 防止 误删释放锁 当业务是5秒 ，锁的过期时间是3秒
            // 不能完全解决误删 因为当我对uuid判断释放锁时，判断通过，
            // 这时我的锁刚好到期，此时删除的就是别人的锁
            //   java代码无法实现        此时用到了  lua脚本

            // 释放锁  (防止误删)
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
                    "then " +
                    "   return redis.call('del', KEYS[1]) " +
                    "else " +
                    "   return 0 " +
                    "end";
            this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class),
                    Arrays.asList("lock"), uuid);
        }
    }
/*    假设 X 线程在 a 方法获取锁之后，继续执行 b 方法，如果此时**不可重入**，线程就必须等待锁释放，再次争抢锁。
    锁明明是被 X 线程拥有，却还需要等待自己释放锁，然后再去抢锁，这看起来就很奇怪，我释放我自己~
    */
             //对加锁  解锁 和自动续期 进行封装提取
       //对上述方法进行提取改造设置了 可重入锁  和实现自动续期
@Autowired
private DistributedLock distributedLock;
    public void testLock2() {
        String uuid = UUID.randomUUID().toString();
        Boolean lock = this.distributedLock.tryLock("lock", uuid, 10);
        if (lock) {
            String num = this.redisTemplate.opsForValue().get("num");
            if (StringUtils.isBlank(num)) {
                this.redisTemplate.opsForValue().set("num", "1");
                return;
            }
            int i = Integer.parseInt(num);
            this.redisTemplate.opsForValue().set("num", String.valueOf(++i));
//              this.testSubLock(uuid);
//            休眠一下，这样就不会删除锁，从而一直重置过期时间，到休眠结束为止
            try {
                TimeUnit.SECONDS.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.distributedLock.unlock("lock", uuid);
        }

    }




//    测试redisson框架
    public void testLock() {
        RLock lock = this.redissonClient.getLock("lock");
        lock.lock();
        try {
            String num = this.redisTemplate.opsForValue().get("num");
            if (StringUtils.isBlank(num)) {
                this.redisTemplate.opsForValue().set("num", "1");
                return;
            }
  /*          try {
                Thread.sleep(10000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
            int i = Integer.parseInt(num);
            this.redisTemplate.opsForValue().set("num", String.valueOf(++i));
        } finally {
            lock.unlock();
        }
    }
    //测试可重入
    public void testSubLock(String uuid){
        this.distributedLock.tryLock("lock", uuid, 30);
        //TODO:
        this.distributedLock.unlock("lock", uuid);
    }
}